package bancosys.tec.services.implementor.descriptor;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import jmine.tec.di.DependencyInjectionMessages;
import jmine.tec.di.cache.TypeMatchLookup;
import jmine.tec.di.resolver.ObjectResolver;
import jmine.tec.di.resolver.impl.StaticObjectResolver;
import jmine.tec.di.scope.TypedScope;
import jmine.tec.di.type.TypeConverter;
import jmine.tec.di.type.TypeConvertionException;
import jmine.tec.di.wrapper.Wrapper;
import jmine.tec.services.ServicesController;
import jmine.tec.utils.Tuple;
import jmine.tec.utils.UnmodifiableTuple;
import jmine.tec.utils.reflection.GenericTypeResolver;
import jmine.tec.utils.reflection.GenericTypeResolverImpl;
import jmine.tec.utils.reflection.fast.FastClass;
import jmine.tec.utils.reflection.fast.FastClassFactory;
import jmine.tec.utils.reflection.fast.FastConstructor;
import jmine.tec.utils.reflection.fast.FastConstructorFactory;
import jmine.tec.utils.reflection.fast.FastMethod;
import jmine.tec.utils.reflection.fast.FastMethodFactory;
import jmine.tec.utils.reflection.fast.asm.AsmFastClassFactory;
import jmine.tec.utils.reflection.fast.asm.FastConstructorCompiler;
import jmine.tec.utils.reflection.fast.asm.FastMethodCompiler;
import jmine.tec.utils.reflection.fast.asm.OpenClassLoader;
import jmine.tec.utils.reflection.fast.reflection.FastMethodImpl;
import jmine.tec.utils.reflection.fast.reflection.ReflectionFastClassFactory;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.transaction.PlatformTransactionManager;

import bancosys.tec.component.Controller;
import bancosys.tec.component.ControllerLocator;
import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.authorization.annotation.DisplayName;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BaseDAOFactory;
import bancosys.tec.persist.dao.BeanNotFoundException;
import bancosys.tec.persist.exception.NaturalKeyNotDefinedException;
import bancosys.tec.services.implementor.ServiceImplementorAccessor;
import bancosys.tec.services.implementor.ServiceImplementorRuntime;

/**
 * Runtime para a api de servicos baseada em anotacoes.
 */
public final class ServiceRuntimeImpl implements ServiceImplementorRuntime, FastMethodFactory, FastConstructorFactory {

    private final TypeMatchLookup<ObjectResolver> staticResolvers = new TypeMatchLookup<ObjectResolver>();

    private final TypeMatchLookup<TypeConverter<?>> typeConvertion = ServiceTypeConverters.initServiceTypeConvertion();

    private final DefaultDescriber defaultDescriber = new DefaultDescriber(this.staticResolvers, this.typeConvertion, this,
            new ByTypeBeanFactoryResolver());

    private BaseDAOFactory baseDAOFactory;

    private ControllerLocator controllerLocator;

    private PlatformTransactionManager transactionManager;

    private ServicesController servicesController;

    private OpenClassLoader classloader;

    private FastClassFactory fcf;

    private FastMethodCompiler methodCompiler;

    private ListableBeanFactory listable;

    private FastConstructorCompiler constructorCompiler;

    {
        try {
            this.classloader = new OpenClassLoader();
            this.fcf = new ReflectionFastClassFactory();
            this.methodCompiler = new FastMethodCompiler(this.classloader);
            this.constructorCompiler = new FastConstructorCompiler(this.classloader);
        } catch (Exception e) {
            System.err.println("cannot create a custom classloader");
        }
    }

    /**
     * 
     */
    public ServiceRuntimeImpl() {
        this.registerResolver(ScopeResolver.getInstance());
        this.registerStaticResolvable(this.typeConvertion);
        this.registerResolver(new ObjectResolver() {

            public boolean isTypeCompatible(Class<?> declaredRawType) {
                return Session.class.equals(declaredRawType);
            }

            public Object resolveObject(TypedScope properties, Wrapper wrapper, Class<?> requiredComponentClass)
                    throws TypeConvertionException {
                BaseDAO<Persistable> dao = ServiceRuntimeImpl.this.baseDAOFactory.getGenericDAO(Persistable.class);
                return wrapper.wrap("<auto>", dao.getSession(), Session.class);
            }

            public boolean isStatic() {
                return false;
            }

            public boolean isResolvable(TypedScope properties) {
                return true;
            }
        });
        this.registerResolver(new ObjectResolver() {

            public boolean isTypeCompatible(Class<?> declaredRawType) {
                return SessionFactory.class.equals(declaredRawType);
            }

            public Object resolveObject(TypedScope properties, Wrapper wrapper, Class<?> requiredComponentClass)
                    throws TypeConvertionException {
                return wrapper.wrap("<auto>", ServiceRuntimeImpl.this.getServicesController().getSessionFactory(), SessionFactory.class);
            }

            public boolean isStatic() {
                return false;
            }

            public boolean isResolvable(TypedScope properties) {
                return true;
            }
        });
        this.registerResolver(new ObjectResolver() {

            public boolean isTypeCompatible(Class<?> declaredRawType) {
                return declaredRawType == BaseDAOFactory.class;
            }

            public Object resolveObject(TypedScope properties, Wrapper wrapper, Class<?> requiredComponentClass)
                    throws TypeConvertionException {
                return ServiceRuntimeImpl.this.getBaseDAOFactory();
            }

            public boolean isStatic() {
                return true;
            }

            public boolean isResolvable(TypedScope properties) {
                return true;
            }
        });
        this.registerResolver(new ObjectResolver() {

            public boolean isTypeCompatible(Class<?> declaredRawType) {
                return BaseDAO.class.equals(declaredRawType);
            }

            @SuppressWarnings("unchecked")
            public Object resolveObject(TypedScope properties, Wrapper wrapper, Class<?> requiredComponentClass)
                    throws TypeConvertionException {
                return ServiceRuntimeImpl.this.getBaseDAOFactory().getGenericDAO((Class<? extends Persistable>) requiredComponentClass);
            }

            public boolean isStatic() {
                return true;
            }

            public boolean isResolvable(TypedScope properties) {
                return true;
            }
        });
        this.registerResolver(new ObjectResolver() {

            public boolean isTypeCompatible(Class<?> declaredRawType) {
                return BaseDAO.class != declaredRawType && BaseDAO.class.isAssignableFrom(declaredRawType);
            }

            @SuppressWarnings("unchecked")
            public Object resolveObject(TypedScope properties, Wrapper wrapper, Class<?> requiredComponentClass)
                    throws TypeConvertionException {
                return ServiceRuntimeImpl.this.getBaseDAOFactory().getDAOByClass((Class<BaseDAO<Persistable>>) requiredComponentClass);
            }

            public boolean isStatic() {
                return true;
            }

            public boolean isResolvable(TypedScope properties) {
                return true;
            }
        });
        this.registerResolver(new RawInputResolver());
    }

    /**
     * @param staticResolvables the static resolvables
     */
    public ServiceRuntimeImpl(Object... staticResolvables) {
        this();
        for (Object object : staticResolvables) {
            if (object != null) {
                this.registerStaticResolvable(object);
            }
        }
    }

    /**
     * Adiciona vários objetos como dependências estáticas.
     * 
     * @param allResolvers the allResolvers to set
     */
    public void setStaticResolvers(List<StaticObjectResolver> allResolvers) {
        this.staticResolvers.registerAll(allResolvers);
    }

    /**
     * @param resolvable {@link List} de {@link Object}
     */
    public void setStaticResolvables(List<Object> resolvable) {
        for (Object object : resolvable) {
            this.staticResolvers.registerNew(new StaticObjectResolver(object));
        }
    }

    /**
     * @param type o tipo do parametro
     * @return {@link ObjectResolver}
     */
    public ObjectResolver findResolverForType(Class<?> type) {
        ObjectResolver lookup = this.staticResolvers.lookup(type);
        if (lookup == null) {
            return null;
        }
        if (lookup.isTypeCompatible(type)) {
            return lookup;
        }
        return null;
    }

    /**
     * @return {@link GenericTypeResolver}
     */
    public GenericTypeResolver getGenericTypeResolver() {
        return GenericTypeResolverImpl.getInstance();
    }

    /**
     * @param resolver {@link ObjectResolver}
     */
    public void registerResolver(ObjectResolver resolver) {
        this.staticResolvers.registerNew(resolver);
    }

    /**
     * Adiciona uma dependencia estatica
     * 
     * @param object {@link Object}
     */
    public void registerStaticResolvable(Object object) {
        this.registerResolver(new StaticObjectResolver(object));
        if (object instanceof ControllerLocator) {
            this.setControllerLocator((ControllerLocator) object);
        } else if (object instanceof BaseDAOFactory) {
            this.setBaseDAOFactory((BaseDAOFactory) object);
        } else if (object instanceof PlatformTransactionManager) {
            this.setTransactionManager((PlatformTransactionManager) object);
        }
    }

    /**
     * Verifica se o tipo dado é um container - tipicamente um array ou uma colecao
     * 
     * @param targetType o tipo
     * @return boolean
     */
    public static boolean isContainerType(Class<?> targetType) {
        return targetType.isArray() || Collection.class.isAssignableFrom(targetType) || Map.class.isAssignableFrom(targetType);
    }

    /**
     * @param locator Controllerlo
     */
    public void setControllerLocator(ControllerLocator locator) {
        this.controllerLocator = locator;
        this.registerResolver(new ControllerResolver(locator));
    }

    /**
     * @param factory {@link BaseDAOFactory}
     */
    public void setBaseDAOFactory(BaseDAOFactory factory) {
        this.baseDAOFactory = factory;
        this.registerTypeConverter(new TypeConverter<Object>() {
            @SuppressWarnings("unchecked")
            public Object convert(String value, Class desiredType) throws TypeConvertionException {
                final BaseDAO dao = ServiceRuntimeImpl.this.baseDAOFactory.getGenericDAO(desiredType);
                try {
                    return dao.findByNaturalKey(value);
                } catch (NaturalKeyNotDefinedException e) {
                    throw new TypeConvertionException(desiredType, e);
                } catch (BeanNotFoundException e) {
                    String expectedType;
                    if (desiredType.getAnnotation(DisplayName.class) != null) {
                        DisplayName displayName = (DisplayName) desiredType.getAnnotation(DisplayName.class);
                        if (displayName.fieldName().equals("")) {
                            expectedType = displayName.value();
                        } else {
                            expectedType = displayName.fieldName();
                        }
                    } else {
                        expectedType = desiredType.getName();
                    }
                    throw new TypeConvertionException(desiredType, value, DependencyInjectionMessages.PROPRIEDADE_NAO_ENCONTRADA.create(
                            expectedType, value));
                }
            }

            public boolean isTypeCompatible(Class<?> type) {
                return Persistable.class.isAssignableFrom(type);
            }
        });
    }

    /**
     * @return {@link BaseDAOFactory}
     */
    public BaseDAOFactory getBaseDAOFactory() {
        return this.baseDAOFactory;
    }

    /**
     * @param type o tipo a ser descrito
     * @return {@link ServiceImplementorAccessor}
     */
    public ServiceImplementorAccessor describeService(Class<?> type) {
        return this.defaultDescriber.describeService(type);
    }

    /**
     * @param beanFactory the bean factory
     */
    public void setBeanFactory(BeanFactory beanFactory) {
        if (beanFactory instanceof ListableBeanFactory) {
            this.listable = (ListableBeanFactory) beanFactory;
        }
    }

    /**
     * @param targetType o tipo alvo
     * @return {@link TypeConverter} ou null se nenhum encontrado
     */
    public TypeConverter<?> findTypeConverterFor(Class<?> targetType) {
        return this.typeConvertion.lookup(targetType);
    }

    /**
     * {@inheritDoc}
     */
    public void registerTypeConverter(TypeConverter<?> converter) {
        this.typeConvertion.registerNew(converter);
    }

    /**
     * Last resort resolver
     * 
     * @author takeshi.fukushima
     */
    private final class ByTypeBeanFactoryResolver implements ObjectResolver {

        private final ConcurrentMap<Class<?>, Tuple<String, Object>> resolverCache =
                new ConcurrentHashMap<Class<?>, Tuple<String, Object>>();

        /**
         * {@inheritDoc}
         */
        public boolean isResolvable(TypedScope properties) {
            return true;
        }

        /**
         * {@inheritDoc}
         */

        public boolean isTypeCompatible(Class<?> declaredRawType) {
            final String[] beanOfType = this.beanOfType(declaredRawType);
            for (String string : beanOfType) {
                if (ServiceRuntimeImpl.this.listable.isSingleton(string)) {
                    return true;
                }
            }
            return false;
        }

        /**
         * @param requiredComponentType the component type
         * @return String[]
         */
        private String[] beanOfType(Class<?> requiredComponentType) {
            return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(ServiceRuntimeImpl.this.listable, requiredComponentType);
        }

        /**
         * {@inheritDoc}
         */
        public Object resolveObject(TypedScope properties, Wrapper wrapper, Class<?> requiredComponentClass) {
            final Tuple<String, Object> cached = this.resolverCache.get(requiredComponentClass);
            if (cached != null) {
                return wrapper.wrap(cached.head(), cached.tail(), requiredComponentClass);
            }
            final String[] beanOfType = this.beanOfType(requiredComponentClass);
            for (String string : beanOfType) {
                if (ServiceRuntimeImpl.this.listable.isSingleton(string)) {
                    Object val = ServiceRuntimeImpl.this.listable.getBean(string);
                    this.resolverCache.putIfAbsent(requiredComponentClass, UnmodifiableTuple.newTuple(string, val));
                    return val;
                }
            }
            return null;
        }

        /**
         * {@inheritDoc}
         */
        public boolean isStatic() {
            return false;
        }
    }

    /**
     * @return the transactionManager
     */
    public PlatformTransactionManager getTransactionManager() {
        return this.transactionManager;
    }

    /**
     * @param transactionManager the transactionManager to set
     */
    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    /**
     * @param classname the classname
     * @return boolean
     */
    public static boolean exists(String classname) {
        try {
            Class.forName(classname, false, ExecutionProxyDescriber.class.getClassLoader());
        } catch (ClassNotFoundException e) {
            return false;
        }
        return true;
    }

    public static final boolean CGLIB_PRESENT;
    static {
        CGLIB_PRESENT = exists("net.sf.cglib.proxy.Enhancer");
    }

    /**
     * @return the controllerLocator
     */
    public ControllerLocator getControllerLocator() {
        return this.controllerLocator;
    }

    /**
     * @return the servicesController
     */
    public ServicesController getServicesController() {
        if (this.servicesController == null && this.controllerLocator != null) {
            Controller forClass = this.controllerLocator.findControllerForClass(ServicesController.class);
            if (forClass instanceof ServicesController) {
                this.servicesController = (ServicesController) forClass;
                this.registerResolver(new ObjectResolver() {

                    private final SessionFactory sessionFactory = ServiceRuntimeImpl.this.servicesController.getSessionFactory();

                    public boolean isResolvable(TypedScope properties) {
                        return true;
                    }

                    public boolean isStatic() {
                        return false;
                    }

                    public Object resolveObject(TypedScope properties, Wrapper wrapper, Class<?> requiredComponentClass)
                            throws TypeConvertionException {
                        return this.sessionFactory.getCurrentSession();
                    }

                    public boolean isTypeCompatible(Class<?> declaredRawType) {
                        return declaredRawType.isAssignableFrom(Session.class);
                    }
                });
                this.registerStaticResolvable(this.servicesController.getSessionFactory());
                this.registerStaticResolvable(this.servicesController);
                this.registerStaticResolvable(this.servicesController.getExportActor());
            }
        }
        return this.servicesController;
    }

    /**
     * @param servicesController the servicesController to set
     */
    public void setServicesController(ServicesController servicesController) {
        this.servicesController = servicesController;
        this.registerStaticResolvable(servicesController);
    }

    /**
     * @return the staticResolvers
     */
    public TypeMatchLookup<ObjectResolver> getStaticResolvers() {
        return this.staticResolvers;
    }

    /**
     * @return the classloader
     */
    public OpenClassLoader getClassloader() {
        return this.classloader;
    }

    /**
     * @param classloader the classloader to set
     */
    public void setClassloader(OpenClassLoader classloader) {
        this.classloader = classloader;
        this.fcf = new AsmFastClassFactory(classloader);
        this.methodCompiler = new FastMethodCompiler(classloader);
        this.constructorCompiler = new FastConstructorCompiler(classloader);
    }

    /**
     * Ativa / desativa compilacao de metodos
     * 
     * @param compile boolean
     */
    public void setCompile(boolean compile) {
        if (!compile) {
            this.fcf = new ReflectionFastClassFactory();
            this.methodCompiler = null;
            this.constructorCompiler = null;
        } else if (this.classloader != null) {
            this.fcf = new AsmFastClassFactory(this.classloader);
            this.methodCompiler = new FastMethodCompiler(this.classloader);
            this.constructorCompiler = new FastConstructorCompiler(this.classloader);
        }
    }

    /**
     * Procura um FastMethod
     * 
     * @param type Class
     * @param method Method
     * @return FastMethod
     */
    public synchronized FastMethod methodAccessorFor(Class<?> type, Method method) {
        FastClass fc = this.fcf.forClass(type);
        FastMethod fm = fc.findMethodFor(method);
        if (fm == null) {
            return new FastMethodImpl(method);
        }
        if (this.methodCompiler != null) {
            return this.methodCompiler.compileAndLoad(fm);
        }
        return fm;
    }

    /**
     * Procura um FastConstructor
     * 
     * @param type Class
     * @param ctor Constructor
     * @return FastConstructor
     */
    public FastConstructor constructorAccessorFor(Class<?> type, Constructor<?> ctor) {
        FastClass fc = this.fcf.forClass(type);
        FastConstructor fctor = fc.findConstructor(ctor);
        if (this.constructorCompiler != null) {
            return this.constructorCompiler.compileAndLoad(fctor);
        }
        return fctor;
    }

    /**
     * {@inheritDoc}
     * 
     * @see jmine.tec.utils.reflection.fast.FastMethodFactory#getMethodAccessorFor(java.lang.reflect.Method)
     */
    public FastMethod getMethodAccessorFor(Method method) {
        return this.methodAccessorFor(method.getDeclaringClass(), method);
    }

    /**
     * {@inheritDoc}
     * 
     * @see jmine.tec.utils.reflection.fast.FastConstructorFactory#getFastConstructorFor(java.lang.reflect.Constructor)
     */
    public FastConstructor getFastConstructorFor(Constructor<?> ctor) {
        return this.constructorAccessorFor(ctor.getDeclaringClass(), ctor);
    }

    /**
     * @return {@link TypeMatchLookup}
     */
    public TypeMatchLookup<TypeConverter<?>> getTypeConvertion() {
        return this.typeConvertion;
    }

    /**
     * @return {@link DefaultDescriber}
     */
    public DefaultDescriber getDefaultDescriber() {
        return this.defaultDescriber;
    }

    /**
     * {@inheritDoc}
     */
    public DependencyDescriber getDependencyDecriber() {
        return this.defaultDescriber.getDependencyDescriber();
    }

}
